-- MIT License
-- 
-- Copyright (c) 2019 monolifed
-- 
-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-- copies of the Software, and to permit persons to whom the Software is
-- furnished to do so, subject to the following conditions:
-- 
-- The above copyright notice and this permission notice shall be included in all
-- copies or substantial portions of the Software.
-- 
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.

local _concat = table.concat
local _sort   = table.sort
local _insert = table.insert
local _remove = table.remove

local _format = string.format
local _sub    = string.sub
local _gsub   = string.gsub
local _find   = string.find
local _gmatch = string.gmatch

local escaped = {
	['&apos;'] = [[']],
	['&quot;'] = [["]],
	['&amp;' ] = [[&]],
	['&gt;'  ] = [[>]],
	['&lt;'  ] = [[<]],
}

-- (table) from attribute string ------------------
local re_attr = [[(%w+)=(["'])(.-)%2]]

local fromstring = function(s)
	local t = {}
	for k, _, v in _gmatch(s, re_attr) do
		for e, c in pairs(escaped) do v = _gsub(v, e, c) end
		t[k] = v
	end
	return t
end

-- (attribute string) from  table --
local fromtable = function(t)
	if not t then return "" end
	local s, sn = {}, 1
	for k, v in pairs(t) do
		v = tostring(v)
		for e, c in pairs(escaped) do v = _gsub(v, c, e) end
		s[sn] = _format(' %s="%s"', k, v); sn = sn + 1
	end
	_sort(s)
	return _concat(s)
end


-- XML string to DOM formatted table --
local re_tag = "<(/?)([%w:]+)(.-)(/?)>" -- ls, tag, attr, rs

local endtag_unexpected = "Unexpected end tag '%s'"
local endtag_mismatched = "Cannot close '%s' with '%s'"
local endtag_missing    = "Missing end tag for '%s'"

-- Based on classic Lua XML parser by Roberto Ierusalimschy
local parse = function(s)
	local stack, size = {}, 0
	local top = {}
	_insert(stack, top); size = size + 1
	
	local si, ei, ls, tag, attr, rs
	local i = 1
	local text
	while true do
		si, ei, ls, tag, attr, rs = _find(s, re_tag, i)
		if not si then break end
		text = _sub(s, i, si - 1)
		if _find(text, "%S") then
			_insert(top, text)
		end
		if rs == "/" then  -- self-closing tag
			_insert(top, {_name = tag, _attr = fromstring(attr), empty = 1})
		elseif ls == "" then   -- start tag
			top = {_name = tag, _attr = fromstring(attr)}
			_insert(stack, top); size = size + 1
		else  -- end tag
			local toclose = _remove(stack); size = size - 1
			top = stack[size]
			if size < 1 then
				return nil, endtag_unexpected:format(tag)
			end
			if toclose._name ~= tag then
				return nil, endtag_mismatched:format(toclose._name, tag)
			end
			_insert(top, toclose)
		end
		i = ei + 1
	end
	text = _sub(s, i)
	if _find(text, "%S") then
		_insert(top, text)
	end
	if size > 1 then
		return nil, endtag_missing:format(top._name)
	end
	return stack[1] --top
end

-- DOM formatted table to XML string --
local _toxml

_toxml = function(t, sub)
	if not sub then sub = {} end
	local tag = t._name
	local attr = fromtable(t._attr)
	if #t > 0 then
		_insert(sub, _format("<%s%s>\n", tag, attr))
		for _, v in ipairs(t) do
			if type(v) == "table" then
				_toxml(v, sub)
			else
				_insert(sub, _format("%s\n", v))
			end
		end
		_insert(sub, _format("</%s>\n", tag))
	else
		_insert(sub, _format("<%s%s/>\n", tag, attr))
	end
	
	return _concat(sub)
end


return {
	parse = parse,
	toxml = function(t) return _toxml(t, {}) end
}

